﻿using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;

namespace ConsoleApplication39
{
    internal class Program
    {
        private static void Main()
        {
            foreach (var modelInfo in M1())
                Console.WriteLine("{0}, {1}", modelInfo.ModelId, modelInfo.ModelCode);
            foreach (var modelInfo in M2())
                Console.WriteLine("{0}, {1}", modelInfo.ModelId, modelInfo.ModelCode);
        }

        private static IEnumerable<IModelInfo> M1()
        {
            var modelCode = "M1";

            var qBuilder = new QBuilder(@"
SELECT  ModelId,
        ModelCode
FROM    Model");
            if (!string.IsNullOrEmpty(modelCode)) qBuilder.Append(@"
WHERE   ModelCode = @modelCode", new { modelCode = modelCode.QParam() });
            return qBuilder.ToQuery<IModelInfo>().Execute();
        }

        private static IEnumerable<IModelInfo> M2()
        {
            var modelCode = Console.ReadLine();

            return Func.Apply(
                new {modelCode = modelCode.QParam(), modelCodeHasValue = !string.IsNullOrEmpty(modelCode)},
                p => {
                    var qBuilder = new QBuilder(@"
SELECT	PersonId,
		Name
FROM	Person");
                    if (p.modelCodeHasValue) qBuilder.Append(@"
WHERE	Name = @modelCode", new {p.modelCode });
                    return qBuilder.ToQuery<IModelInfo>();
                }).Execute();
        }
    }

    public static class ParamExtensions
    {
        public static IQParam<T> QParam<T>(this T it)
        {
            throw new NotImplementedException();
        }
    }

    public interface IQParam<T>
    {
    }

    public static class QueryExtensions
    {
        public static IEnumerable<T> Execute<T>(this IQuery<T> it)
        {
            using (var sqlCommand = new SqlCommand())
            {
                sqlCommand.CommandText = it.TransformText();
                using (var connection = new SqlConnection(ConnectionString))
                {
                    connection.Open();
                    sqlCommand.Connection = connection;
                    using (var sqlDataReader = sqlCommand.ExecuteReader())
                        while (sqlDataReader.Read())
                            yield return it.GetReader(sqlDataReader).Materialize();
                }
            }
        }

        public static string ConnectionString
        {
            get { return @"Data Source=(local);Initial Catalog=ControllableQuery;Integrated Security=True"; }
        }
    }

    public static class QBuilderExtensions
    {
        public static IQuery<T> ToQuery<T>(this QBuilder it)
        {
            throw new NotImplementedException();
        }

        public static IDataReader<T> GetReader<T>(this IQuery<T> it, SqlDataReader reader)
        {
            return new DataReader<T>(reader);
        }

        private class DataReader<T> : IDataReader<T>
        {
            private readonly SqlDataReader sqlDataReader;

            public SqlDataReader SqlDataReader
            {
                get { return sqlDataReader; }
            }

            public DataReader(SqlDataReader sqlDataReader)
            {
                this.sqlDataReader = sqlDataReader;
            }
        }
    }

    public interface IQuery<T>
    {
        string TransformText();
    }

    public class QBuilder
    {
        public QBuilder(string s)
        {
            throw new NotImplementedException();
        }

        public QBuilder Append<T>(string s, T o)
        {
            throw new NotImplementedException();
        }
    }

    public static class Func
    {
        public static TResult Apply<T, TResult>(T arg, Func<T, TResult> func)
        {
            return func(arg);
        }
    }

    public interface IDataReader<T>
    {
        SqlDataReader SqlDataReader { get; }
    }

    public interface IModelInfo
    {
        int ModelId { get; }
        int ModelCode { get; }
    }

    public static class Materializer
    {
        public static T Materialize<T>(this IDataReader<T> reader)
        {
            return Cache<T>.Func(reader.SqlDataReader);
        }

        private static class Cache<T>
        {
            public static readonly Func<SqlDataReader, T> Func;

            static Cache()
            {
                Func = DynamicMaterializer.Build<SqlDataReader, T>(
                    type =>
                    {
                        if (type == typeof(string))
                            return typeof(Materializer).GetMethod("GetString");
                        else if (type == typeof(Guid))
                            return typeof(Materializer).GetMethod("GetGuid");
                        else if (type == typeof(int))
                            return typeof(Materializer).GetMethod("GetInt");
                        else
                            throw new NotImplementedException();
                    });
            }
        }

        public static string GetString(SqlDataReader reader, string fieldName)
        {
            return reader.GetString(reader.GetOrdinal(fieldName));
        }

        public static Guid GetGuid(SqlDataReader reader, string fieldName)
        {
            return reader.GetGuid(reader.GetOrdinal(fieldName));
        }

        public static int GetInt(SqlDataReader reader, string fieldName)
        {
            return reader.GetInt32(reader.GetOrdinal(fieldName));
        }
    }

    public static class DynamicMaterializer
    {
        private static readonly ModuleBuilder moduleBuilder;

        static DynamicMaterializer()
        {
            var assemblyName = new AssemblyName { Name = Guid.NewGuid().ToString() };
            moduleBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run).
                DefineDynamicModule(assemblyName.Name);
        }

        public static Func<T, string, object> Qwe2<T>(PropertyInfo propertyInfo)
        {
            var dynamicMethod = new DynamicMethod(Guid.NewGuid().ToString(),
                                      typeof(object),
                                      new[] { typeof(T), typeof(string) },
                                      true);

            var ilGenerator = dynamicMethod.GetILGenerator();
            ilGenerator.DeclareLocal(propertyInfo.PropertyType);
            ilGenerator.Emit(OpCodes.Ldarg_0);
            ilGenerator.EmitCall(OpCodes.Callvirt, propertyInfo.GetGetMethod(), null);
            ilGenerator.Emit(OpCodes.Stloc_0);
            ilGenerator.Emit(OpCodes.Ldloc_0);
            ilGenerator.Emit(OpCodes.Ldarg_1);
            ilGenerator.EmitCall(OpCodes.Callvirt, propertyInfo.PropertyType.GetProperty("Name").GetSetMethod(), null);
            ilGenerator.Emit(OpCodes.Ldloc_0);
            ilGenerator.Emit(OpCodes.Ret);

            dynamicMethod.DefineParameter(1, ParameterAttributes.In, "arg1");
            dynamicMethod.DefineParameter(2, ParameterAttributes.In, "arg2");

            return (Func<T, string, object>)dynamicMethod.CreateDelegate(typeof(Func<T, string, object>));
        }

        public static bool IsAnonymousType(Type type)
        {
            var customAttributes = type.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false);
            switch (customAttributes.Length)
            {
                case 0:
                    return false;
                case 1:
                    return type.Name.Contains("AnonymousType");
                default:
                    throw new ApplicationException();
            }
        }

        public static Func<T, TResult> Build<T, TResult>(Func<Type, MethodInfo> func)
        {
            Type type;
            if (typeof(TResult).IsInterface)
            {
                var typeBuilder = moduleBuilder.DefineType(
                    typeof(TResult).Name + "_" + Guid.NewGuid(), TypeAttributes.NotPublic, null,
                    new[] { typeof(TResult) });

                foreach (var property in typeof(TResult).GetProperties())
                {
                    CreateProperty(typeBuilder, property);
                }

                type = typeBuilder.CreateType();
            }
            else
            {
                type = typeof(TResult);
            }

            var dynamicMethod = new DynamicMethod(Guid.NewGuid().ToString(),
                                                  type,
                                                  new[] { typeof(T) },
                                                  true);

            var ilGenerator = dynamicMethod.GetILGenerator();
            if (IsAnonymousType(type))
            {
                var constructorInfo = type.GetConstructors()[0];
                foreach (var parameterInfo in constructorInfo.GetParameters())
                {
                    ilGenerator.Emit(OpCodes.Ldarg_0);
                    ilGenerator.Emit(OpCodes.Ldstr, parameterInfo.Name);
                    ilGenerator.EmitCall(OpCodes.Call, func(parameterInfo.ParameterType), null);
                }
                ilGenerator.Emit(OpCodes.Newobj, constructorInfo);
            }
            else
            {
                ilGenerator.DeclareLocal(type);
                ilGenerator.Emit(OpCodes.Newobj, type.GetConstructor(new Type[] { }));
                ilGenerator.Emit(OpCodes.Stloc_0);
                var properties = type.GetProperties();
                foreach (var propertyInfo in properties)
                {
                    ilGenerator.Emit(OpCodes.Ldloc_0);
                    ilGenerator.Emit(OpCodes.Ldarg_0);
                    ilGenerator.Emit(OpCodes.Ldstr, propertyInfo.Name);
                    ilGenerator.EmitCall(OpCodes.Call, func(propertyInfo.PropertyType), null);
                    ilGenerator.EmitCall(OpCodes.Callvirt, propertyInfo.GetSetMethod(), null);
                }
                ilGenerator.Emit(OpCodes.Ldloc_0);
            }
            ilGenerator.Emit(OpCodes.Ret);

            dynamicMethod.DefineParameter(1, ParameterAttributes.In, "arg1");

            return (Func<T, TResult>)dynamicMethod.CreateDelegate(typeof(Func<T, TResult>));
        }

        private static void CreateProperty(TypeBuilder typeBuilder, PropertyInfo propertyInfo)
        {
            var fieldBuilder = typeBuilder.DefineField(
                GetPascalCase(propertyInfo.Name), propertyInfo.PropertyType, FieldAttributes.Private);

            var propertyBuilder = typeBuilder.DefineProperty(
                propertyInfo.Name, PropertyAttributes.None, propertyInfo.PropertyType, null);

            const MethodAttributes getSetMethodAttributes =
                MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig
                | MethodAttributes.SpecialName;

            var getterMethodBuilder = typeBuilder.DefineMethod("get_" + propertyInfo.Name,
                                                               getSetMethodAttributes,
                                                               propertyInfo.PropertyType,
                                                               Type.EmptyTypes);

            var getterIlGenerator = getterMethodBuilder.GetILGenerator();
            getterIlGenerator.Emit(OpCodes.Ldarg_0);
            getterIlGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
            getterIlGenerator.Emit(OpCodes.Ret);

            var setterMethodBuilder = typeBuilder.DefineMethod("set_" + propertyInfo.Name,
                                                               getSetMethodAttributes,
                                                               null,
                                                               new[] { propertyInfo.PropertyType });

            var setterIlGenerator = setterMethodBuilder.GetILGenerator();
            setterIlGenerator.Emit(OpCodes.Ldarg_0);
            setterIlGenerator.Emit(OpCodes.Ldarg_1);
            setterIlGenerator.Emit(OpCodes.Stfld, fieldBuilder);
            setterIlGenerator.Emit(OpCodes.Ret);

            propertyBuilder.SetGetMethod(getterMethodBuilder);
            propertyBuilder.SetSetMethod(setterMethodBuilder);
        }

        private static string GetPascalCase(string name)
        {
            return name.Substring(0, 1).ToLower() + name.Substring(1);
        }
    }
}